﻿
using System;
using System.Collections.Generic;
using System.Linq;


namespace Boilen.Primitives.Members {

    /// <summary>
    /// Writes code for an implementation member.
    /// </summary>
    public abstract class Member : IWritable {

        public static readonly Member Empty = new EmptyMember( );

        private readonly string name_;

        private Doc doc_;


        /// <summary>
        /// Gets the name of the member.
        /// </summary>
        public string Name { get { return this.name_; } }

        /// <summary>
        /// Gets or sets the documentation for the member.
        /// </summary>
        public Doc Doc {
            get { return this.doc_; }
            set {
                if( !object.Equals( this.doc_, value ) ) {
                    this.doc_ = value;
                    this.OnDocChanged( );
                }
            }
        }

        /// <summary>
        /// Gets or sets the conditional compilation symbol for the member.
        /// </summary>
        public CompilationSymbol Condition { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether a new line should be inserted between the output of adjacent members.
        /// </summary>
        public bool SpaceOutput { get; set; }

        /// <summary>
        /// Gets a value indicating whether documentation should appear inline with the member, or manually written elsewhere.
        /// </summary>
        public virtual bool InlineDoc { get { return true; } }

        /// <summary>
        /// Gets a value indicating the extensions used by the member, such as <see cref="Guard"/>s,
        /// <see cref="System.Windows.Freezable"/> types, or dependency property validation.
        /// </summary>
        public abstract Extensions UsedExtensions { get; }


        /// <summary>
        /// Initializes a new <see cref="Member"/> instance.
        /// </summary>
        /// <param name="name">The name of the member.</param>
        protected Member( string name ) {
            Ensure.NotNullOrEmpty( name );
            this.name_ = name;
        }


        /// <summary>
        /// Assigns the value used for the <see cref="Condition"/> property.
        /// </summary>
        public Member SetCondition( CompilationSymbol condition ) {
            this.Condition = condition;
            return this;
        }


        /// <inheritdoc/>
        public void Write( ICodeWriter writer ) {
            Ensure.NotNull( writer );

            this.WriteDoc( writer );
            this.PreWrite( writer );
            this.WriteCore( writer );
            this.PostWrite( writer );
        }

        /// <inheritdoc cref="Write"/>
        protected abstract void WriteCore( ICodeWriter writer );

        /// <summary>Called before <see cref="PreWrite"/>.</summary>
        protected virtual void WriteDoc( ICodeWriter writer ) {
            if( this.InlineDoc && this.Doc != null )
                this.Doc.Write( writer );
        }

        /// <summary>Called before <see cref="WriteCore"/>.</summary>
        protected virtual void PreWrite( ICodeWriter writer ) { }

        /// <summary>Called after <see cref="WriteCore"/>.</summary>
        protected virtual void PostWrite( ICodeWriter writer ) { }

        /// <summary>
        /// Called whenever the <see cref="Member.Doc"/> property changes.
        /// </summary>
        protected virtual void OnDocChanged( ) { }


        /// <summary>
        /// Writes each member in the specified collection.
        /// </summary>
        public static void WriteMembers<T>( ICodeWriter writer, IEnumerable<T> members, Action<int, bool> separate )
            where T : Member {
            Ensure.NotNull( writer, members );

            // Order all top-level Members by type.
            var groupedMembers = members.GroupBy( m =>
                m is EmptyMember ? -1 :
                m is ConstantMember ? 0 :
                m is FieldMember ? 1 :
                m is AccessorMember ? 2 :
                m is MethodMember ? 3 :
                m is MemberGroup ? 4 :
                5
            )
            .Where( g => g.Key >= 0 )
            .OrderBy( g => g.Key )
            .ToArray( );

            // If all conditions are the same, combine members into a single group.
            var allGroups = groupedMembers.SelectMany( g => g );
            var allConditions = allGroups.Select( m => m.Condition ).ToArray( );
            var firstCondition = allConditions.FirstOrDefault( );
            if( !firstCondition.Equals( CompilationSymbol.None ) && allConditions.All( c => c.Equals( firstCondition ) ) ) {
                WriteConditionalMembers( writer, allGroups, separate, null );
            }
            // Write out each group of members, separated by newlines.
            else {
                Util.Iterate(
                    groupedMembers,
                    ( groupIndex, lastGroup ) => writer.WriteLine( ),
                    ( groupIndex, group ) => WriteConditionalMembers( writer, group, separate, null )
                );
            }
        }
        public static void WriteMembers<T>( ICodeWriter writer, IEnumerable<T> members ) where T : Member { WriteMembers( writer, members, null ); }

        /// <summary>
        /// Writes the <see cref="Member.Doc"/> for each member in the specified collection.
        /// </summary>
        public static void WriteMemberDocumentation<T>( ICodeWriter writer, IEnumerable<T> members )
            where T : Member {
            Ensure.NotNull( writer, members );

            var docs = members
                .Select( p => p.Doc )
                .Where( d => d != null );
            foreach( Doc doc in docs ) {
                doc.Write( writer );
            }
        }

        /// <summary>
        /// Writes each member in the specified collection, grouped by <see cref="Condition"/>.
        /// </summary>
        public static void WriteConditionalMembers<T>( ICodeWriter writer, IEnumerable<T> members, Action<int, bool> separate, Action<IGrouping<CompilationSymbol, T>[], int> customizeGroup )
            where T : Member {
            // Group by condition, distinguishing overriding and unconditional members.
            IGrouping<CompilationSymbol, T>[] conditionalMembers = members
                .OrderBy( member => member.Condition )
                .GroupBy( member => member.Condition )
                .ToArray( );
            if( conditionalMembers.Length == 0 )
                return;

            if( customizeGroup != null )
                customizeGroup( conditionalMembers, -1 );

            // Write each group of conditionals.
            bool anyUnconditionalMembers = conditionalMembers.Any( g => g.Key.IsUnconditional );
            for( int i = 0; i < conditionalMembers.Length; ++i ) {
                var conditionalGroup = conditionalMembers[i];
                var condition = conditionalGroup.Key;
                var previousCondition = i == 0 ? CompilationSymbol.None : conditionalMembers[i - 1].Key;
                var nextCondition = i + 1 == conditionalMembers.Length ? CompilationSymbol.None : conditionalMembers[i + 1].Key;

                // Get the preprocessor condition appropriate to the current group and symbol.
                string conditionFormat;
                if( condition.IsUnconditional )
                    conditionFormat = "";
                else if( previousCondition.Symbol == condition.Symbol )
                    conditionFormat = "#else";
                else
                    conditionFormat = "#if {0}{1}";

                // Write conditional group opening.
                bool hasCondition = !string.IsNullOrEmpty( conditionFormat );
                if( hasCondition )
                    writer.WriteLineUnindented( conditionFormat, condition.IsElseCondition ? "!" : "", condition.Symbol );
                if( customizeGroup != null )
                    customizeGroup( conditionalMembers, i );

                // Write conditional group members.
                Util.Iterate(
                    conditionalGroup,
                    ( memberIndex, lastMember ) => {
                        if( separate != null )
                            separate( memberIndex, lastMember );
                        if( conditionalGroup.ElementAt( memberIndex ).SpaceOutput )
                            writer.WriteLine( );
                    },
                    ( memberIndex, member ) => member.Write( writer )
                );

                // Write conditional group closing.
                bool last = (i + 1 == conditionalMembers.Length);
                if( anyUnconditionalMembers && !last && separate != null )
                    separate( -1, true );

                bool closeCondition = hasCondition && (last || nextCondition.Symbol != condition.Symbol);
                if( closeCondition ) {
                    writer.WriteUnindented( "#endif" );
                    if( customizeGroup == null )
                        writer.WriteLine( );
                }
            }
        }


        private sealed class EmptyMember : Member {
            public EmptyMember( ) : base( "<empty>" ) { }

            /// <inheritdoc/>
            public override Extensions UsedExtensions { get { return Extensions.None; } }

            /// <inheritdoc/>
            protected override void WriteCore( ICodeWriter writer ) { }
        }

    }

}
